<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgpu - angular slicing</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="example.css">
	</head>
	<body>

		<div id="info">
			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>

			<div class="title-wrapper">
				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Angular Slicing</span>
			</div>

			<small>
				Based on <a href="https://threejs-journey.com/lessons/sliced-model-shader" target="_blank" rel="noopener">Three.js Journey</a> lesson.
			</small>
		</div>

		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.webgpu.js",
					"three/webgpu": "../build/three.webgpu.js",
					"three/tsl": "../build/three.tsl.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

		<script type="module">

			import * as THREE from 'three/webgpu';
			import { If, TWO_PI, atan, color, frontFacing, output, positionLocal, Fn, uniform, vec4 } from 'three/tsl';

			import { Inspector } from 'three/addons/inspector/Inspector.js';

			import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
			import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

			let camera, scene, renderer, controls;

			init();

			function init() {

				camera = new THREE.PerspectiveCamera( 35, window.innerWidth / window.innerHeight, 0.1, 100 );
				camera.position.set( - 5, 5, 12 );

				scene = new THREE.Scene();

				// environment

				const hdrLoader = new UltraHDRLoader();
				hdrLoader.load( 'textures/equirectangular/royal_esplanade_2k.hdr.jpg', ( environmentMap ) => {

					environmentMap.mapping = THREE.EquirectangularReflectionMapping;

					scene.background = environmentMap;
					scene.environment = environmentMap;

				} );

				// lights

				const directionalLight = new THREE.DirectionalLight( '#ffffff', 4 );
				directionalLight.position.set( 6.25, 3, 4 );
				directionalLight.castShadow = true;
				directionalLight.shadow.mapSize.set( 2048, 2048 );
				directionalLight.shadow.camera.near = 0.1;
				directionalLight.shadow.camera.far = 30;
				directionalLight.shadow.camera.top = 8;
				directionalLight.shadow.camera.right = 8;
				directionalLight.shadow.camera.bottom = - 8;
				directionalLight.shadow.camera.left = - 8;
				directionalLight.shadow.normalBias = 0.05;
				scene.add( directionalLight );

				// TSL functions
			
				const inAngle = Fn( ( [ position, angleStart, angleArc ] ) => {

					const angle = atan( position.y, position.x ).sub( angleStart ).mod( TWO_PI ).toVar();
					return angle.greaterThan( 0 ).and( angle.lessThan( angleArc ) );

				} );

				// materials

				const defaultMaterial = new THREE.MeshPhysicalNodeMaterial( {
					metalness: 0.5,
					roughness: 0.25,
					envMapIntensity: 0.5,
					color: '#858080'
				} );

				const slicedMaterial = new THREE.MeshPhysicalNodeMaterial( {
					metalness: 0.5,
					roughness: 0.25,
					envMapIntensity: 0.5,
					color: '#858080',
					side: THREE.DoubleSide
				} );

				// uniforms

				const sliceStart = uniform( 1.75 );
				const sliceArc = uniform( 1.25 );
				const sliceColor = uniform( color( '#b62f58' ) );

				// output

				slicedMaterial.outputNode = Fn( () => {

					// discard

					inAngle( positionLocal.xy, sliceStart, sliceArc ).discard();

					// backface color

					const finalOutput = output;
					If( frontFacing.not(), () => {

						finalOutput.assign( vec4( sliceColor, 1 ) );
			
					} );
			
					return finalOutput;
			
				} )();

				// shadow

				slicedMaterial.castShadowNode = Fn( () => {

					// discard

					inAngle( positionLocal.xy, sliceStart, sliceArc ).discard();

					return vec4( 0, 0, 0, 1 );
			
				} )();

				// model

				const dracoLoader = new DRACOLoader();
				dracoLoader.setDecoderPath( 'jsm/libs/draco/' );

				const gltfLoader = new GLTFLoader();
				gltfLoader.setDRACOLoader( dracoLoader );

				gltfLoader.load( './models/gltf/gears.glb', ( gltf ) => {

					const model = gltf.scene;

					model.traverse( ( child ) => {

						if ( child.isMesh ) {

							if ( child.name === 'outerHull' )
								child.material = slicedMaterial;
							else
								child.material = defaultMaterial;

							child.castShadow = true;
							child.receiveShadow = true;
			
						}

					} );

					scene.add( model );
			
				} );

				// plane

				const plane = new THREE.Mesh(
					new THREE.PlaneGeometry( 10, 10, 10 ),
					new THREE.MeshStandardMaterial( { color: '#aaaaaa' } )
				);
				plane.receiveShadow = true;
				plane.position.set( - 4, - 3, - 4 );
				plane.lookAt( new THREE.Vector3( 0, 0, 0 ) );
				scene.add( plane );

				// renderer

				renderer = new THREE.WebGPURenderer( { antialias: true } );
				renderer.toneMapping = THREE.ACESFilmicToneMapping;
				renderer.toneMappingExposure = 1;
				renderer.shadowMap.enabled = true;
				renderer.inspector = new Inspector();
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.setAnimationLoop( animate );
				document.body.appendChild( renderer.domElement );

				// controls

				controls = new OrbitControls( camera, renderer.domElement );
				controls.enableDamping = true;
				controls.minDistance = 0.1;
				controls.maxDistance = 50;

				// events

				window.addEventListener( 'resize', onWindowResize );

				// debug

				const gui = renderer.inspector.createParameters( 'Parameters' );
				gui.add( sliceStart, 'value', - Math.PI, Math.PI, 0.001 ).name( 'sliceStart' );
				gui.add( sliceArc, 'value', 0, Math.PI * 2, 0.001 ).name( 'sliceArc' );
				gui.addColor( { color: sliceColor.value.getHexString( THREE.SRGBColorSpace ) }, 'color' ).onChange( value => sliceColor.value.set( value ) );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			async function animate() {
			
				controls.update();

				renderer.render( scene, camera );

			}

		</script>
	</body>
</html>
